{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Metaclasses vs Class Decorators"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As we have seen, class decorators can achieve a lot of the metaprogramming goals we might have."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But there is one area where they fall short of metaclasses - inheritance."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Metaclasses are carried through inheritance, whereas decorators are not."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's go back to the previous class decorator example we had (and I'll use the original one to keep the code simple):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "from functools import wraps\n",
    "\n",
    "def func_logger(fn):\n",
    "    @wraps(fn)\n",
    "    def inner(*args, **kwargs):\n",
    "        result = fn(*args, **kwargs)\n",
    "        print(f'log: {fn.__qualname__}({args}, {kwargs}) = {result}')\n",
    "        return result\n",
    "    return inner    \n",
    "\n",
    "def class_logger(cls):\n",
    "    for name, obj in vars(cls).items():\n",
    "        if callable(obj):\n",
    "            print('decorating:', cls, name)\n",
    "            setattr(cls, name, func_logger(obj))\n",
    "    return cls"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And as we saw, we can decorate a class with it:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "@class_logger\n",
    "class Person:\n",
    "    def __init__(self, name, age):\n",
    "        self.name = name\n",
    "        self.age = age\n",
    "    \n",
    "    def greet(self):\n",
    "        return f'Hello, my name is {self.name} and I am {self.age}'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "Person('Alex', 10).greet()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We could do this with a metaclass too:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "class ClassLogger(type):\n",
    "    def __new__(mcls, name, bases, class_dict):\n",
    "        new_cls = super().__new__(mcls, name, bases, class_dict)\n",
    "        for key, obj in vars(new_cls).items():\n",
    "            if callable(obj):\n",
    "                setattr(new_cls, key, func_logger(obj))\n",
    "        return new_cls        "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person(metaclass=ClassLogger):\n",
    "    def __init__(self, name, age):\n",
    "        self.name = name\n",
    "        self.age = age\n",
    "    \n",
    "    def greet(self):\n",
    "        return f'Hello, my name is {self.name} and I am {self.age}'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "log: Person.__init__((<__main__.Person object at 0x7f9be0ce18d0>, 'John', 78), {}) = None\n",
      "log: Person.greet((<__main__.Person object at 0x7f9be0ce18d0>,), {}) = Hello, my name is John and I am 78\n"
     ]
    }
   ],
   "source": [
    "p = Person('John', 78).greet()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So, why not just use a class decorator?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's see how inheritance works with both those methods."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's do the decorator approach first:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "decorating: <class '__main__.Person'> __init__\n",
      "decorating: <class '__main__.Person'> greet\n"
     ]
    }
   ],
   "source": [
    "@class_logger\n",
    "class Person:\n",
    "    def __init__(self, name, age):\n",
    "        self.name = name\n",
    "        self.age = age\n",
    "    \n",
    "    def greet(self):\n",
    "        return f'Hello, my name is {self.name} and I am {self.age}'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's inherit `Person`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Student(Person):\n",
    "    def __init__(self, name, age, student_number):\n",
    "        super().__init__(name, age)\n",
    "        self.student_number = student_number\n",
    "        \n",
    "    def study(self):\n",
    "        return f'{self.name} studies...'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "log: Person.__init__((<__main__.Student object at 0x7f9be0cec790>, 'Alex', 19), {}) = None\n"
     ]
    }
   ],
   "source": [
    "s = Student('Alex', 19, 'abcdefg')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So first off, you can see that the print worked, but only for the `__init__` in the `Person` class, no logs were generated for the `__init__` in the `Student` class."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "By the same token, we don't get logging on the `study` method:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Alex studies...'"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s.study()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So we would need to remember to decorate the `Student` class as well:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "decorating: <class '__main__.Student'> __init__\n",
      "decorating: <class '__main__.Student'> study\n"
     ]
    }
   ],
   "source": [
    "@class_logger\n",
    "class Student(Person):\n",
    "    def __init__(self, name, age, student_number):\n",
    "        super().__init__(name, age)\n",
    "        self.student_number = student_number\n",
    "        \n",
    "    def study(self):\n",
    "        return f'{self.name} studies...'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "log: Person.__init__((<__main__.Student object at 0x7f9be0ce1090>, 'Alex', 19), {}) = None\n",
      "log: Student.__init__((<__main__.Student object at 0x7f9be0ce1090>, 'Alex', 19, 'abcdefg'), {}) = None\n"
     ]
    }
   ],
   "source": [
    "s = Student('Alex', 19, 'abcdefg')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "log: Person.greet((<__main__.Student object at 0x7f9be0ce1090>,), {}) = Hello, my name is Alex and I am 19\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "'Hello, my name is Alex and I am 19'"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s.greet()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "log: Student.study((<__main__.Student object at 0x7f9be0ce1090>,), {}) = Alex studies...\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "'Alex studies...'"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s.study()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So, we just have to remember to decorate **every** subclass as well."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But if we use a metaclass, watch what happens when inherit:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person(metaclass=ClassLogger):\n",
    "    def __init__(self, name, age):\n",
    "        self.name = name\n",
    "        self.age = age\n",
    "    \n",
    "    def greet(self):\n",
    "        return f'Hello, my name is {self.name} and I am {self.age}'\n",
    "    \n",
    "class Student(Person):\n",
    "    def __init__(self, name, age, student_number):\n",
    "        super().__init__(name, age)\n",
    "        self.student_number = student_number\n",
    "        \n",
    "    def study(self):\n",
    "        return f'{self.name} studies...'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "log: Person.__init__((<__main__.Student object at 0x7f9be0cff210>, 'Alex', 19), {}) = None\n",
      "log: Student.__init__((<__main__.Student object at 0x7f9be0cff210>, 'Alex', 19, 'abcdefg'), {}) = None\n"
     ]
    }
   ],
   "source": [
    "s = Student('Alex', 19, 'abcdefg')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "log: Student.study((<__main__.Student object at 0x7f9be0cff210>,), {}) = Alex studies...\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "'Alex studies...'"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s.study()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This works because `Student` inherits from `Person`, and since `Person` uses a metaclass for the creation, this follows down to the `Student` class as well."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "__main__.ClassLogger"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "type(Person)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "__main__.ClassLogger"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "type(Student)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see the type of both the parent and the subclass is `ClassLogger` even though we did not explicitly state that `Student` shouls use the metaclass for creation.\n",
    "\n",
    "It happened automatically because we did not have a `__new__` method in the `Student` class, so the parent's `__new__` was essentially used, and that one uses the metaclass."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can see this more explicitly this way:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Student(Person):\n",
    "    def __new__(cls, name, age, student_number):\n",
    "        return super().__new__(cls)\n",
    "    \n",
    "    def __init__(self, name, age, student_number):\n",
    "        super().__init__(name, age)\n",
    "        self.student_number = student_number\n",
    "        \n",
    "    def study(self):\n",
    "        return f'{self.name} studies...'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "log: Person.__init__((<__main__.Student object at 0x7f9be0d041d0>, 'Alex', 19), {}) = None\n",
      "log: Student.__init__((<__main__.Student object at 0x7f9be0d041d0>, 'Alex', 19, 'ABC'), {}) = None\n"
     ]
    }
   ],
   "source": [
    "s = Student('Alex', 19, 'ABC')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "log: Student.study((<__main__.Student object at 0x7f9be0d041d0>,), {}) = Alex studies...\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "'Alex studies...'"
      ]
     },
     "execution_count": 22,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s.study()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "One of the disadvantages of metaclasses vs class decorators is that only a \"single\" metaclass can be used. (Actually it's a bit more subtle than that, we can use a different metaclass in for a subclass if the metclass is a subclass of the parent's metaclass - we'll cover this point again when we look at multiple inheritance.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Metaclass1(type):\n",
    "    pass\n",
    "\n",
    "class Metaclass2(type):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person(metaclass=Metaclass1):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [
    {
     "ename": "TypeError",
     "evalue": "metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
      "\u001b[0;32m<ipython-input-28-b20e37c31e76>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0;32mclass\u001b[0m \u001b[0mStudent\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mPerson\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmetaclass\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mMetaclass2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m      2\u001b[0m     \u001b[0;32mpass\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
      "\u001b[0;31mTypeError\u001b[0m: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases"
     ]
    }
   ],
   "source": [
    "class Student(Person, metaclass=Metaclass2):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see we cannot specify a custom metaclass for `Student` because that would conflict with the class it is inheriting from."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "An exception is if we inherit from a parent who has `type` as its metaclass:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    pass\n",
    "\n",
    "class Student(Person, metaclass=Metaclass1):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Person()\n",
    "s = Student()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(type, __main__.Metaclass1)"
      ]
     },
     "execution_count": 31,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "type(Person), type(Student)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "It can also cause problems in multiple inheritance.\n",
    "\n",
    "We haven't covered multiple inheritance yet, but let me show you the issue at least:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Class1(metaclass=Metaclass1):\n",
    "    pass\n",
    "\n",
    "class Class2(metaclass=Metaclass2):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here we have created two classes that use different custom metaclasses.\n",
    "\n",
    "If we try to create a new class that inherits from both:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [
    {
     "ename": "TypeError",
     "evalue": "metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
      "\u001b[0;32m<ipython-input-33-4f34dea54f0b>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0;32mclass\u001b[0m \u001b[0mMultiClass\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mClass1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mClass2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m      2\u001b[0m     \u001b[0;32mpass\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
      "\u001b[0;31mTypeError\u001b[0m: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases"
     ]
    }
   ],
   "source": [
    "class MultiClass(Class1, Class2):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Again, if one of the base classes is `type` and the other is a custom metaclass, then this is allowed (this is because `Metaclass1` is itself a subclass of `type`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Class1(metaclass=type):\n",
    "    pass\n",
    "\n",
    "class Class2(metaclass=Metaclass1):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MultiClass(Class1, Class2):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "On the other hand we can stack decorators as much as we want (we just have to be careful with the order in which we stack them sometimes)."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
